package com.ideaaedi.component.dump;

import com.ideaaedi.commonds.bash.BashUtil;
import com.ideaaedi.commonds.io.IOUtil;
import com.ideaaedi.commonds.jar.JarUtil;
import com.ideaaedi.commonds.path.PathUtil;
import lombok.extern.slf4j.Slf4j;

import java.io.File;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;

/**
 * 与{@link ExitDumpClassExecutor}相比，此工具类dump结束后不会退出程序，其余注意事项见{@link ExitDumpClassExecutor}
 *
 * @author JustryDeng
 * @since 2021/9/25 19:40:06
 */
@Slf4j
public class NonExitDumpClassExecutor {
    
    /** 与pom中的<version>2.6.0</version>同 */
    public static final String VERSION = "2.6.0";
    
    public static final String EXEC_DUMP_JAR = String.format("component-dump-class-%s.jar", VERSION);
    
    /**
     * @see NonExitDumpClassExecutor#exec(String, String)
     */
    public static Map<String, byte[]> exec(String includePrefixes) {
        return exec(BashUtil.currPid(), includePrefixes);
    }
    
    /**
     * @see NonExitDumpClassExecutor#exec(String, String, String)
     */
    public static Map<String, byte[]> exec(String pid, String includePrefixes) {
        return exec(pid, includePrefixes, null);
    }
    
    /**
     * dump 指定JVM中的class
     *
     * @param pid
     *         要被进行dump操作的JVM进程id
     * @param includePrefixes
     *         要dump的class的全类名前缀，多个使用逗号分割
     * @param excludePrefixes
     *         明确排除dump的class的全类名前缀，多个使用逗号分割
     *
     * @return key - 全类名,   value - class字节码
     */
    public static Map<String, byte[]> exec(String pid, String includePrefixes, String excludePrefixes) {
        // 获取当前类所在的jar包的路径作为agentJarPath
        String execDumpJarPath = PathUtil.getProjectRootDir(NonExitDumpClassExecutor.class);
        if (!execDumpJarPath.endsWith(EXEC_DUMP_JAR)) {
            // 那么有可能component-dump-class作为lib包存在
            if (!execDumpJarPath.endsWith(".jar")) {
                // 找不到component-dump-class jar包
                throw new IllegalStateException(String.format("So, where is %s ?", EXEC_DUMP_JAR));
            }
            String tmpDir = execDumpJarPath.substring(0, execDumpJarPath.lastIndexOf("/") + 1) + "_temp_/component"
                    + "-dump-class/";
            List<String> list = new ArrayList<>();
            list.add("/lib/" + EXEC_DUMP_JAR);
            list.add("/BOOT-INF/lib/" + EXEC_DUMP_JAR);
            List<String> execJarPathList = JarUtil.unJarWar(execDumpJarPath, tmpDir, true, list);
            if (execJarPathList == null || execJarPathList.size() == 0) {
                // 找不到component-dump-class jar包
                throw new IllegalStateException(String.format("So, where is %s ?", EXEC_DUMP_JAR));
            }
            execDumpJarPath = execJarPathList.get(0);
        }
        
        String outputDir =
                execDumpJarPath.substring(0, execDumpJarPath.lastIndexOf("/") + 1) + "dump_" + System.currentTimeMillis() + "/";
        
        String consoleInfoFormat = "\n[INFO] dump-class-process consoleInfo start ------------------------------------------------------------\n"
                + "%s"
                + "[INFO] dump-class-process consoleInfo  end  ------------------------------------------------------------\n";
        // 单独起一个java进程，来执行dump, 该进程执行完dump后，会自己结束程序
        final String finalExecDumpJarPath = execDumpJarPath;
        try {
            String consoleInfo = CompletableFuture.supplyAsync(() -> BashUtil.runBash(String.format("java -jar %s "
                            + "pid=%s includePrefixes=%s excludePrefixes=%s outputDir=%s keepLongName=%s",
                    finalExecDumpJarPath, pid == null ? "" : pid, includePrefixes == null ? "" : includePrefixes,
                    excludePrefixes == null ? "" : excludePrefixes, outputDir, true))).get(30, TimeUnit.SECONDS);
            log.info(String.format(consoleInfoFormat, consoleInfo));
        } catch (Exception e) {
            log.info(String.format(consoleInfoFormat, e.getMessage()));
            return Collections.emptyMap();
        }
        File dir = new File(outputDir);
        if (!dir.exists()) {
            return Collections.emptyMap();
        }
        TreeMap<String, byte[]> classBytes = IOUtil.listFileOnly(dir, ".class").stream()
                .collect(
                        Collectors.toMap(
                                // ".class".length() == 6
                                file -> file.getName().substring(0, file.getName().length() - 6),
                                IOUtil::toBytes,
                                (pre, next) -> next,
                                TreeMap::new
                        )
                );
        try {
            IOUtil.delete(dir);
        } catch (Exception e) {
            // ignore
        }
        return classBytes;
    }
}
